package ro.flashbite.loaders
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.getTimer;
	
	import ro.flashbite.helpers.ArrayHelpers;
	import ro.flashbite.helpers.DictionaryHelpers;
	import ro.flashbite.helpers.MathHelpers;
	import ro.flashbite.interfaces.ISleepWakeupDestroy;
	import ro.flashbite.loaders.events.AssetEvent;
	import ro.flashbite.loaders.events.AssetManagerEvent;
	import ro.flashbite.loaders.types.*;
	import ro.flashbite.loaders.types.consts.AssetTypes;
	import ro.flashbite.loggers.ILogger;
	import ro.flashbite.loggers.LoggerFactory;
	
	/**
	 * 
	 * This manager will load assets(sounds, swf, pictures, xmls)
	 * 
	 * @author Adrian Barbu
	 * 
	 */	
	public final class AssetManager extends EventDispatcher implements ISleepWakeupDestroy
	{
		/**
		 * the max count of assets that are loading at time...
		 */	
		public static const MAX_PARALLEL_DOWNLOADS	:	uint = 10;
		
		/**
		 * this holds all assets info
		 */		
		private var 		_data					:	Dictionary;
		/**
		 * this is the list of assets that will be loaded 
		 */		
		private var 		_toLoadList				:	Array;
		/**
		 * this is the list of assets that are curently loading
		 */	
		private var 		_realLoadingList		:	Array;
		/**
		 * this is the list of assets that are waiting to load
		 */	
		private var 		_waitingToLoadList		:	Array;
		/**
		 * logger for debug purpose 
		 */		
		private var 		_logger					:	ILogger;
		/**
		 * if in sleep mode, it will not dispatch events 
		 */		
		private var 		_eventsToDispatch		:	Array;
		/**
		 * tells if is in sleep mode 
		 */		
		private var 		_isSleeping				:	Boolean;
		/**
		 * the size of all assets 
		 */		
		private var 		_totalSize				:	int;
		/**
		 * the total of loading at some point 
		 */		
		private var 		_currentLoadedSize		:	int;
		/**
		 * holds the id regarding wich level the asset is from 
		 */		
		private var 		_moduleID				:	String;
		/**
		 * holds a reference to all loaded assets; used on big destoy 
		 */		
		private var 		_allLoadedAssets		:	Array;
		/**
		 * holds a reference to all currently loading assets; used for loading percentage
		 */		
		private var 		_allCurrentlyLoadingAssets:	Array;
		/**
		 * holds total time of loading assets
		 */
		private var 		_loadingTime			:	uint;
		/**
		 * tells if the asset will be reloaded each time the page make a refresh
		 */		
		private var			_webReloadMode			:	Boolean;
		/**
		 * relative path for usage 
		 */		
		private var 		_relativePath			:	String;
		
		//==================================================================================================
		// CONSTRUCTOR
		//==================================================================================================
		
		public function AssetManager(webReloadMode:Boolean = false, relativePath:String = "") 
		{
			_webReloadMode = webReloadMode;
			_relativePath = relativePath;
			_logger = LoggerFactory.getLogger("AssetManager");
			_eventsToDispatch = [];
		}
		
		//==================================================================================================
		// PUBLIC
		//==================================================================================================
		
		/**
		 *
		 * assets.xml will be parsed here to create the list of assets to be loaded
		 *  
		 * @param xml
		 * 
		 */		
		public function initFromXml(xml:XML, totalSize:Number, moduleId:String):void
		{
			//try to stop listeners on current loading assets
			if (_realLoadingList) {
				for each (var asset:Asset in _realLoadingList) {
					removeAssetListeners(asset);
				}
				_realLoadingList = [];	
			}
			
			if (_toLoadList) {
				for each (asset in _toLoadList) 
					asset.destroy();
				_toLoadList = [];
			}
			if (_waitingToLoadList) {
				for each (asset in _waitingToLoadList) 
					asset.destroy();
				_waitingToLoadList = [];
			}
			
			if (_eventsToDispatch) {
				for each (var event:AssetManagerEvent in _eventsToDispatch) {
					event.destroy();
				}
				_eventsToDispatch = [];
			}
			//unload any items previously loaded
			unloadModule(moduleId);
			
			
			_toLoadList = [];
			//now will load from specific list
			if (xml) {
				var xmlList	: XMLList = xml.asset;
				
				for each (var subXml:XML in xmlList) {
					asset = createAssetByXml(subXml);
					_toLoadList.push(asset);
				}
			}
			
			if (!_allLoadedAssets) {
				_allLoadedAssets = [];
			}	
			//reset currently loaded assets
			_allCurrentlyLoadingAssets = [];
			
			_moduleID = moduleId;
			
			_totalSize = totalSize;
			
			_logger.debug("Initialized from XML : _moduleID = " + _moduleID + ", _totalSize = " + _totalSize);
		}
		
		public function startLoad():void
		{
			_loadingTime = getTimer();
			
			if (!_data)	{
				_data = new Dictionary(false);
			}
			if (!_data[_moduleID]) {
				_data[_moduleID] = new Dictionary(false);
			}
			
			_data[_moduleID][AssetTypes.MP3] = new Dictionary(false);
			_data[_moduleID][AssetTypes.SWF] = new Dictionary(false);
			_data[_moduleID][AssetTypes.XML] = new Dictionary(false);
			
			realStartLoading();
		}
		
		public function unloadModule(moduleID:String):void
		{
			//reset currently loading assets
			if (_realLoadingList) {
				for each (var asset:Asset in _realLoadingList) {
					removeAssetListeners(asset);
					asset.destroy();
				}
				_realLoadingList = [];	
			}
			
			if (_data && _data[moduleID]) {
				destroyAssetsFromDict(_data[moduleID][AssetTypes.SWF]);
				destroyAssetsFromDict(_data[moduleID][AssetTypes.MP3]);
				destroyAssetsFromDict(_data[moduleID][AssetTypes.XML]);
				
				_data[moduleID] = null;
				_logger.debug("Unload complete on moduleID = " + moduleID);
			}
			
			//reset also event list
			_eventsToDispatch = [];
		}
		
		public function destroyAsset(id:String, type:String, moduleId:String):Boolean
		{
			if (_data && _data[moduleId] && _data[moduleId][type] && _data[moduleId][type][id]) {
				var asset:Asset = _data[moduleId][type][id] as Asset;
				asset.destroy();
				_data[moduleId][type][id] = null;
				delete _data[moduleId][type][id];
				_logger.debug("Asset with '" + id + "' destroyed");
				return true;
			} else {
				_logger.error("Id provided '" + id + "' or type unrecognized");
				return false
			}
		}
		public function loadSpecificAsset(xml:XML, moduleId:String):void
		{
			_toLoadList = [];
			_allLoadedAssets = [];
			_realLoadingList = [];
			
			var asset:Asset = createAssetByXml(xml);
			_toLoadList.push(asset);
			
			_moduleID = moduleId;
			
			realStartLoading();
		}
		
		//==================================================================================================
		// ISleepWakeupDestroy
		//==================================================================================================
		
		public function sleep():void
		{
			if (_isSleeping) return;
			
			_logger.debug("SLEEP");
			
			_isSleeping = true;
		}
		
		public function wakeup():void
		{
			if (!_isSleeping) return;
			
			_isSleeping = false;
			
			_logger.debug("WAKEUP");
			
			//dispatch old events from sleep mode
			if (_eventsToDispatch && _eventsToDispatch.length > 0) {
				
				//check to see if the last event is the AssetManagerEvent.LOADING_COMPLETE; if so, just dispatch them
				//else, dispatch them and continue loading
				var lastEvent:Event = _eventsToDispatch[_eventsToDispatch.length - 1],
					isCompleteLoading:Boolean = (lastEvent is AssetManagerEvent) && (lastEvent.type == AssetManagerEvent.LOADING_COMPLETE)
				
				//dispatch them in order
				var event:AssetManagerEvent,
					i:uint,
					len:uint = _eventsToDispatch.length;	
				for (i = 0; i < len; i++) {
					event = _eventsToDispatch[i] as AssetManagerEvent;
					dispatchEvent(event);
					event.destroy();
				}
					
				_eventsToDispatch = [];
				
				
				if (isCompleteLoading == false) {
					checkEmptySpot();
				}
			}
		}
		
		public function destroy():void
		{
			if (_logger) _logger.debug("Destroy called");
			
			//try to stop listeners on current loading assets
			if (_realLoadingList) {
				for each (var asset:Asset in _realLoadingList) {
					removeAssetListeners(asset);
				}
				_realLoadingList = null;	
			}
			
			if (_data) {
				DictionaryHelpers.clear(_data);
				_data = null;
			}
			
			if (_allLoadedAssets) {
				for each (asset in _allLoadedAssets) 
					asset.destroy();
				_allLoadedAssets = null;
			}
		
			if (_toLoadList) {
				for each (asset in _toLoadList) 
					asset.destroy();
				_toLoadList = null;
			}
			if (_waitingToLoadList) {
				for each (asset in _waitingToLoadList) 
				asset.destroy();
				_waitingToLoadList = null;
			}
			
			_logger = null;
			if (_eventsToDispatch) {
				for each (var event:AssetManagerEvent in _eventsToDispatch) {
					event.destroy();
				}
				_eventsToDispatch = null;
			}
			
			_moduleID = null;
			
			_allCurrentlyLoadingAssets = null;
		}
		
		//==================================================================================================
		// GETTERS
		//==================================================================================================
		
		/**
		 *
		 * Returns an asset
		 *  
		 * @param id = the asset name
		 * @param type = the type of the asset : swf, mp3, xml
		 * @param moduleId = the level name
		 * @return 
		 * 
		 */		
		public function getAsset(id:String, type:String, moduleId:String):Asset
		{
			if (_data && _data[moduleId] && _data[moduleId][type] && _data[moduleId][type][id])
				return _data[moduleId][type][id];
			
			_logger.error("Id provided '" + id + "' or type unrecognized");
			
			return null;	
		}
		
		//==================================================================================================
		// PRIVATE
		//==================================================================================================
		
		/**
		 *
		 * This will check if an Asset is corectly constructed
		 *  
		 * @param type
		 * @param id
		 * @param url
		 * 
		 */		
		private function checkAsset(type:int, id:String, url:String):void
		{
			
		}
		
		private function createAssetByXml(xml:XML):Asset
		{
			const id:String = String(xml.@id),
				  type:String = String(xml.@type),
				  path:String = String(xml.@path);
			var asset:Asset;
			switch (type) {
				case AssetTypes.MP3:
					asset = new SoundAsset(String(xml.@channelID));
					break;
				case AssetTypes.SWF:
					asset = new SwfAsset();
					break;
				case AssetTypes.XML:
					asset = new XmlAsset();
					break;
				default:
					throw new Error("Asset id " + id  + "unrecognized; Please use data from AssetConsts");
					break;									
			} 
			asset.init(type, id, _relativePath + path, true, _webReloadMode);
			
			return asset;
		}
		
		private function destroyAssetsFromDict(dict:Dictionary):void
		{
			if (dict) {
				var assetID:String,
				asset:Asset;
				for (assetID in dict) {
					asset = dict[assetID];
					if (asset) {
						try {
							ArrayHelpers.removeValue(_allLoadedAssets, asset);
						} catch (e:Error) {}
						removeAssetListeners(asset);
						asset.destroy();
					}
					dict[assetID] = null;
					delete dict[assetID];
				}
				dict = null;
			}
		}
		
		//----------- EVENTS ON LOADING ASSET --------------------------------------------------------------
		
		private function addAssetListeners(asset:Asset):void
		{
			asset.addEventListener(AssetEvent.OPEN, 			onAssetEvent, 	false, 0, false	);
			asset.addEventListener(AssetEvent.PROGRESS, 		onAssetEvent, 	false, 0, false	);
			asset.addEventListener(AssetEvent.COMPLETE, 		onAssetEvent, 	false, 0, false	);
			
			asset.addEventListener(AssetEvent.IO_ERROR, 		onAssetEvent, 	false, 0, false	);
			asset.addEventListener(AssetEvent.SECURITY_ERROR, 	onAssetEvent, 	false, 0, false	);
			asset.addEventListener(AssetEvent.HTTP_STATUS, 		onAssetEvent, 	false, 0, false	);
		}
		
		private function removeAssetListeners(asset:Asset):void
		{
			asset.removeEventListener(AssetEvent.OPEN, 				onAssetEvent	);
			asset.removeEventListener(AssetEvent.PROGRESS, 			onAssetEvent	);
			asset.removeEventListener(AssetEvent.COMPLETE, 			onAssetEvent	);
			
			asset.removeEventListener(AssetEvent.IO_ERROR, 			onAssetEvent	);
			asset.removeEventListener(AssetEvent.SECURITY_ERROR, 	onAssetEvent	);
			asset.removeEventListener(AssetEvent.HTTP_STATUS, 		onAssetEvent	);
		}
		
		private function onAssetEvent(e:AssetEvent):void
		{
			var assetTarget	: Asset 	= e.currentTarget as Asset,
				assetEvent	: * 		= e.getEvent();
			
			switch (e.type) {
				case AssetEvent.OPEN:
					break;
				case AssetEvent.PROGRESS:
					//calculate stats....
					var asset:Asset;
						//total loaded assets size.....
					var totalLoaded:uint = 0;
					if (_allCurrentlyLoadingAssets.length > 0) {
						for each (asset in _allCurrentlyLoadingAssets) {
							totalLoaded += asset.getBytesTotal();
						}
					}
						//total loading assets size...
					var totalLoading:uint = 0;
					if (_realLoadingList.length > 0) {
						for each (asset in _realLoadingList) {
							totalLoading += asset.getBytesLoaded();
						}
					}
					
					_currentLoadedSize = totalLoaded + totalLoading;
					
					//dispatching loading percentage
					var evAssetProgress:AssetManagerEvent = new AssetManagerEvent(AssetManagerEvent.PROGRESS, MathHelpers.roundToDecimals(_currentLoadedSize/_totalSize*100, 2));
					if (_isSleeping == false) {
						dispatchEvent(evAssetProgress);
						evAssetProgress.destroy();
					} else _eventsToDispatch.push(evAssetProgress);
					
					break;
				case AssetEvent.COMPLETE:
					_logger.debug("Asset stats : " + assetTarget.getLoadingInfo());
					//remove listeners
					removeAssetListeners(assetTarget);
					//saving reference for calculate loading percentage
					_allCurrentlyLoadingAssets.push(assetTarget);
					//saving a reference..to be destroyed.
					_allLoadedAssets.push(assetTarget);
					//remove asset from _realLoadingList
					if (ArrayHelpers.removeValue(_realLoadingList, assetTarget) == false) 
						_logger.error("Couldn't remove asseet from _realLoadingList");
					
					//save it in database
					_data[_moduleID][assetTarget.getType()][assetTarget.getId()] = assetTarget;
					
					//dispatching one asset complete
					var evAssetComplete:AssetManagerEvent = new AssetManagerEvent(AssetManagerEvent.ASSET_COMPLETE, assetTarget);
					if (_isSleeping == false) {
						dispatchEvent(evAssetComplete);
						evAssetComplete.destroy();
						
						//check if we have some waiting asset to load
						checkEmptySpot();
					} else {
						_eventsToDispatch.push(evAssetComplete);
						
						//just wait until wakeup occurs to checkEmptySpot
					}
					break;
				case AssetEvent.IO_ERROR:
					_logger.error("IO_ERROR on asset " + assetTarget.getId() + ", url : " + assetTarget.getUrlReq().url);
					break;
				case AssetEvent.SECURITY_ERROR:
					_logger.error("SECURITY_ERROR on asset " + assetTarget.getId());
					break;
				case AssetEvent.HTTP_STATUS:
					break;
				default:
					_logger.error("This should not happen. Event '" + e.type + "' unrecognized");
					break						
			}
		}
		
		//-------------- LOADING ---------------------------------------------------------------------------
		
		private function realStartLoading():void
		{
			_waitingToLoadList = _toLoadList.concat();
			_realLoadingList = [];
			
			var downloadCount:uint = Math.min(_waitingToLoadList.length, MAX_PARALLEL_DOWNLOADS);
			//add assets to loading list
			for (var i:uint = 0; i < downloadCount; i++) {
				_realLoadingList.push(_waitingToLoadList[i]);
			}
			//removing them from _waitingToLoadList
			for each (var asset:Asset in _realLoadingList) {
				if (ArrayHelpers.removeValue(_waitingToLoadList, asset) == false) 
					_logger.error("Couldn't remove asseet from _waitingToLoadList");
			}
			//_waitingToLoadList.splice(0, downloadCount);
			
			//start loading
			for each (asset in _realLoadingList) {
				addAssetListeners(asset);
				asset.startLoad();
			}
		}
		
		private function checkEmptySpot():void
		{
			if (_waitingToLoadList.length > 0) {
				//remove one asset from waiting to load list
				var toLoadAsset:Asset = _waitingToLoadList.shift();
				//push it in _realLoadingList
				_realLoadingList.push(toLoadAsset);
				//start loading
				addAssetListeners(toLoadAsset);
				toLoadAsset.startLoad();
			} else {
				if (_realLoadingList.length > 0) {
					//still assets to load
				} else {
					//complete....
					_loadingTime = getTimer() - _loadingTime;
					_logger.debug("Loading procedure complete in " + _loadingTime + " ms.");
					
					//recalculate totalSize
					var totalSizeLoaded:uint = 0;
					for each (var asset:Asset in _allCurrentlyLoadingAssets) {
						totalSizeLoaded += asset.getBytesTotal();
					}
					_logger.debug("Total size given  : " + _totalSize);
					_logger.debug("Total size loaded : " + totalSizeLoaded);
					
					var speed:Number = MathHelpers.roundToDecimals(/*MathHelpers.bytesToKb(*/totalSizeLoaded/*)*//_loadingTime * 1000 / 1024, 3);
					_logger.debug("Average speed : " + speed + " Kb/Sec");
					
					//reset arrays...
					_waitingToLoadList = [];
					_realLoadingList = [];
					_toLoadList = [];
					
					//dispatch all loading complete
					var evAllComplete:AssetManagerEvent = new AssetManagerEvent(AssetManagerEvent.LOADING_COMPLETE, _loadingTime, speed);
					if (_isSleeping == false) {
						dispatchEvent(evAllComplete);
						evAllComplete.destroy();
					} else _eventsToDispatch.push(evAllComplete);
				}
			}
		}
	}
}